package nl.windgazer.arachne.services.facebook;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.tapestry5.Asset;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Path;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.services.Environment;
import org.apache.tapestry5.services.javascript.InitializationPriority;

/**
 * A Serious attempt to get an easy to use light-weight Facebook client running in T5.
 * 
 * @author mreuring
 *
 */
public class FacebookAPISettingsImp implements FacebookAPISettings {

	private boolean render = false;

	private final String api;
	private final String app;
	private final String secret;

	private final HttpServletRequest request;
	private final HttpServletResponse response;

	private boolean cookie;
	private boolean status;
	private boolean xfbml;
	private boolean reload;

	private HashMap<InitializationPriority, StringBuilder> inits = new HashMap<InitializationPriority, StringBuilder>();
	private final HashMap<String, String> cookieValues = new HashMap<String, String>();

	final private Asset jsValidationFix;

	public FacebookAPISettingsImp(
			@Inject @Symbol(FacebookAPISettings.API) final String api
			, @Inject @Symbol(FacebookAPISettings.APP) final String app
			, @Inject @Symbol(FacebookAPISettings.APP_SECRET) final String secret
			, @Inject @Symbol(FacebookAPISettings.COOKIE_DEFAULT) final boolean cookie
			, @Inject @Symbol(FacebookAPISettings.STATUS_DEFAULT) final boolean status
			, @Inject @Symbol(FacebookAPISettings.XFBML_DEFAULT) final boolean xfbml
			, @Inject @Symbol(FacebookAPISettings.RELOAD_ON_SESSIONCHANGE_DEFAULT) final boolean reload
			, @Inject @Path(FacebookAPISettings.JS_XFBML_FIX) final Asset jsValidationFix
			, @Inject HttpServletRequest request
			, @Inject HttpServletResponse response
	) {
		this.api = api;
		this.app = app;
		this.secret = secret;
		this.cookie = cookie;
		this.status = status;
		this.xfbml = xfbml;
		this.reload = reload;
		this.request = request;
		this.response = response;
		parseCookie();
		this.jsValidationFix = jsValidationFix;
	}

	public void setRender(boolean render) {
		this.render = render;
	}

	public boolean getRender() {
		return render;
	}

	public void setCookie(boolean cookie) {
		this.cookie = cookie;
	}

	public void setStatus(boolean status) {
		this.status = status;
	}

	public void setXFBML(boolean xfbml) {
		this.xfbml = xfbml;
	}

	public void setSessionReload(boolean reload) {
		this.reload = reload;
	}

	public void addInit(String init, InitializationPriority priority) {
		if (!this.inits.containsKey(priority)) {
			inits.put(priority, new StringBuilder());
		}
		final StringBuilder sb = inits.get(priority);
		sb.append('\n');
		sb.append(init);
	}

	/**
	 * Overrides default XFBML init to true, set render to true and add the correct
	 * js call for parsing XFBML.
	 */
	public void setXFBMLParse(final String id, final String tag, String ... params) {
		if (!this.xfbml) {
			setXFBML(true);
			addInit("FB.XFBML.parse();", InitializationPriority.LATE);
		}

		final StringBuilder init = new StringBuilder();
		init.append("span2XFBML('")
		.append(id)
		.append("', '")
		.append(tag)
		.append("'");
		
		for (final String param : params) {
			init.append(", '")
			.append(param)
			.append('\'');
		}
		
		init.append(");");

		addInit(init.toString(), InitializationPriority.EARLY);
		setRender(true);
	}

	public void render(MarkupWriter writer, Environment _environment) {
		if (render) {

			final StringBuilder attribs = new StringBuilder();
			if (!FacebookAPISettings.APP_NULL.equals(app)) {
				attribs.append("appId:'")
				.append(app).append("', ");
			};
			if (!FacebookAPISettings.API_NULL.equals(api)) {
				attribs.append("apiKey:'")
				.append(api).append("', ");
			};
			attribs.append("status:")
			.append(status);
			attribs.append(", cookie:")
			.append(cookie);
			attribs.append(", xfbml:")
			.append(xfbml);
			attribs.append(", reloadIfSessionStateChanged:")
			.append(reload);

			final StringBuilder inits = new StringBuilder();
			if (this.inits.containsKey(InitializationPriority.IMMEDIATE)) {
				inits.append(this.inits.get(InitializationPriority.IMMEDIATE));
			}
			if (this.inits.containsKey(InitializationPriority.EARLY)) {
				inits.append(this.inits.get(InitializationPriority.EARLY));
			}
			if (this.inits.containsKey(InitializationPriority.NORMAL)) {
				inits.append(this.inits.get(InitializationPriority.NORMAL));
			}
			if (this.inits.containsKey(InitializationPriority.LATE)) {
				inits.append(this.inits.get(InitializationPriority.LATE));
			}

			final StringBuilder script = new StringBuilder();
			script
			.append("window.fbAsyncInit = function() {\n");
			if (!isVerified()) {
			script.append("	FB.Event.subscribe('auth.login', function(response) {window.location.reload()});");
			}
			script
			.append("    FB.init({")
			.append(attribs)
			.append("});\n")
			.append(inits)
			.append("  };\n")
			.append("  (function() {\n")
			.append("    var e = document.createElement('script');\n    e.async = true;\n")
			.append("    e.src = document.location.protocol +\n")
			.append("      '//connect.facebook.net/en_US/all.js';\n")
			.append("    document.getElementById('fb-root').appendChild(e);\n")
			.append("  }());");

			Element body = writer.getDocument().find("html/body");
			body.element("script", "src", jsValidationFix.toClientURL());
			body.element("div", "id", "fb-root");
			body.element("script", "script", "text/javascript").cdata(script.toString());
		}
	}

	private void parseCookie(String value) {
		if (value!=null) {
			final StringBuffer payload = new StringBuffer();
			String[] split = value.split("&");
			Arrays.sort(split);
			for (final String pair:split) {
				try {
					final String decoded = URLDecoder.decode(pair, "UTF-8");
					String[] values = decoded.split("=");
					cookieValues.put(values[0], values[1]);
					if (!values[0].equals("sig")) {
						payload.append(decoded);
					}
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
				}
			}
			//payload.append(secret);
			final String pl = payload.toString();
			final String md5 = generateSignature(pl, secret);//MD5(payload.toString());
//			final String sig = cookieValues.get("sig");
			cookieValues.put("localsig", md5);
			cookieValues.put("verified", String.valueOf(md5.equals(cookieValues.get("sig"))));
		}
	}

	/**
	 * Doesn't seem the produce the same result as what Facebook is sending (or inputting the wrong payload of course)
	 * 
	 * @param requestString
	 * @param secretKey
	 * @return
	 */
	public static String generateSignature(String payload, String secretKey){
		try {
			final String requestString = payload+secretKey;
			StringBuilder result = new StringBuilder();
			MessageDigest md = MessageDigest.getInstance("MD5");
			md.reset();
			md.update(requestString.getBytes());
			for (byte b : md.digest()) {
				String hex=Integer.toHexString(0xff & b);
				if(hex.length()==1) result.append('0');
				result.append(hex);
//				result.append(Integer.toHexString((b & 0xf0) >>> 4));
//				result.append(Integer.toHexString(b & 0x0f));
			}
			return(result.toString());
		} catch (NoSuchAlgorithmException e) {
			return("Error: no MD5 ");
		}

	}

	//"access_token=130487793676962%7C2.Ks1L5XpP8o79pX_0EPAmZQ__.3600.1292810400-100001530730489%7Cqg7jiTMhlishlKCHUR2pzwIaCOs
	//&base_domain=windgazer.nl
	//&expires=1292810400&secret=MbL0RlBF2X8ljTSk7BLDow__
	//&session_key=2.Ks1L5XpP8o79pX_0EPAmZQ__.3600.1292810400-100001530730489
	//&sig=8a8b59c83046642048d5e50c7fd6b269
	//&uid=100001530730489"

	//Concat all the values except sig together sorted by key, without the & symbols
	//Then verify MD5 against the resulting string using the application secret and compare it against the value of sig

	public String readCookieValue(final String cookieKey) {
		final Cookie[] cookies = request.getCookies();
		if (cookies!=null) {
			for (final Cookie cookie:request.getCookies()) {
				if (cookieKey.equals(cookie.getName())) return cookie.getValue();
			}
		}
		return null;
	}

	public void parseCookie() {
		String value = readCookieValue("fbs_" + app);
		parseCookie(value);
	}

	public void clearFBCookies() {
		final Cookie[] cookies = request.getCookies();
		if (cookies!=null) {
	        final String domain = request.getServerName().replaceAll("^[^\\.]+", "");
			for (final Cookie cookie:request.getCookies()) {
				if (cookie.getName().startsWith("fbs_")) {
					final Cookie cc = new Cookie(cookie.getName(), API_NULL);
					cc.setMaxAge(0);
					cc.setPath("/");
					cc.setDomain(domain);
					response.addCookie(cc); //This 'should' delete the pre-existing cookie.....
				}
			}
		}
	}

	/**
	 * Should contain:
	 * access_token=130487793676962%7C2.AQBhCzgNHKYUZ2ip.3600.1306342800.1-525556615%7C_rWHa48c2FjUHg1S81Vm2HT_ezU
	 * &base_domain=windgazer.nl
	 * &expires=1306342800
	 * &secret=AQCTcAdSC7MTephp
	 * &session_key=2.AQBhCzgNHKYUZ2ip.3600.1306342800.1-525556615
	 * &sig=7ae666e784eed339fcc22038875e1e7a
	 * &uid=525556615
	 */
	public HashMap<String, String> getSettings() {
		return cookieValues;
	}

	public boolean isVerified() {
		return cookieValues!=null && Boolean.TRUE.toString().equals(cookieValues.get("verified"));
	}

}
